﻿// <file>
//     <copyright see="prj:///doc/copyright.txt"/>
//     <license see="prj:///doc/license.txt"/>
//     <owner name="Mike Krüger" email="mike@icsharpcode.net"/>
//     <version>$Revision$</version>
// </file>

using System.Diagnostics;
using System.Text;

namespace ICSharpCode.TextEditor.Document
{
    public sealed class TextUtilities
    {
        public enum CharacterType
        {
            LetterDigitOrUnderscore,
            WhiteSpace,
            Other
        }

        /// <remarks>
        ///     This function takes a string and converts the whitespace in front of
        ///     it to tabs. If the length of the whitespace at the start of the string
        ///     was not a whole number of tabs then there will still be some spaces just
        ///     before the text starts.
        ///     the output string will be of the form:
        ///     1. zero or more tabs
        ///     2. zero or more spaces (less than tabIndent)
        ///     3. the rest of the line
        /// </remarks>
        public static string LeadingWhiteSpaceToTabs(string line, int tabIndent)
        {
            var sb = new StringBuilder(line.Length);
            var consecutiveSpaces = 0;
            int i;
            for (i = 0; i < line.Length; i++)
                if (line[i] == ' ')
                {
                    consecutiveSpaces++;
                    if (consecutiveSpaces == tabIndent)
                    {
                        sb.Append(value: '\t');
                        consecutiveSpaces = 0;
                    }
                }
                else if (line[i] == '\t')
                {
                    sb.Append(value: '\t');
                    // if we had say 3 spaces then a tab and tabIndent was 4 then
                    // we would want to simply replace all of that with 1 tab
                    consecutiveSpaces = 0;
                }
                else
                {
                    break;
                }

            if (i < line.Length)
                sb.Append(line.Substring(i - consecutiveSpaces));
            return sb.ToString();
        }

        public static bool IsLetterDigitOrUnderscore(char c)
        {
            if (!char.IsLetterOrDigit(c))
                return c == '_';
            return true;
        }

        /// <remarks>
        ///     This method returns the expression before a specified offset.
        ///     That method is used in code completion to determine the expression given
        ///     to the parser for type resolve.
        /// </remarks>
        public static string GetExpressionBeforeOffset(TextArea textArea, int initialOffset)
        {
            var document = textArea.Document;
            var offset = initialOffset;
            while (offset - 1 > 0)
                switch (document.GetCharAt(offset - 1))
                {
                    case '\n':
                    case '\r':
                    case '}':
                        goto done;
//                        offset = SearchBracketBackward(document, offset - 2, '{','}');
//                        break;
                    case ']':
                        offset = SearchBracketBackward(document, offset - 2, openBracket: '[', closingBracket: ']');
                        break;
                    case ')':
                        offset = SearchBracketBackward(document, offset - 2, openBracket: '(', closingBracket: ')');
                        break;
                    case '.':
                        --offset;
                        break;
                    case '"':
                        if (offset < initialOffset - 1)
                            return null;
                        return "\"\"";
                    case '\'':
                        if (offset < initialOffset - 1)
                            return null;
                        return "'a'";
                    case '>':
                        if (document.GetCharAt(offset - 2) == '-')
                        {
                            offset -= 2;
                            break;
                        }

                        goto done;
                    default:
                        if (char.IsWhiteSpace(document.GetCharAt(offset - 1)))
                        {
                            --offset;
                            break;
                        }

                        var start = offset - 1;
                        if (!IsLetterDigitOrUnderscore(document.GetCharAt(start)))
                            goto done;

                        while (start > 0 && IsLetterDigitOrUnderscore(document.GetCharAt(start - 1)))
                            --start;
                        var word = document.GetText(start, offset - start).Trim();
                        switch (word)
                        {
                            case "ref":
                            case "out":
                            case "in":
                            case "return":
                            case "throw":
                            case "case":
                                goto done;
                        }

                        if (word.Length > 0 && !IsLetterDigitOrUnderscore(word[index: 0]))
                            goto done;
                        offset = start;
                        break;
                }
            done:
            //// simple exit fails when : is inside comment line or any other character
            //// we have to check if we got several ids in resulting line, which usually happens when
            //// id. is typed on next line after comment one
            //// Would be better if lexer would parse properly such expressions. However this will cause
            //// modifications in this area too - to get full comment line and remove it afterwards
            if (offset < 0)
                return string.Empty;

            var resText = document.GetText(offset, textArea.Caret.Offset - offset).Trim();
            var pos = resText.LastIndexOf(value: '\n');
            if (pos >= 0)
                offset += pos + 1;
            var expression = document.GetText(offset, textArea.Caret.Offset - offset).Trim();
            return expression;
        }

        public static CharacterType GetCharacterType(char c)
        {
            if (IsLetterDigitOrUnderscore(c))
                return CharacterType.LetterDigitOrUnderscore;
            if (char.IsWhiteSpace(c))
                return CharacterType.WhiteSpace;
            return CharacterType.Other;
        }

        public static int GetFirstNonWSChar(IDocument document, int offset)
        {
            while (offset < document.TextLength && char.IsWhiteSpace(document.GetCharAt(offset)))
                ++offset;
            return offset;
        }

        public static int FindWordEnd(IDocument document, int offset)
        {
            var line = document.GetLineSegmentForOffset(offset);
            var endPos = line.Offset + line.Length;
            while (offset < endPos && IsLetterDigitOrUnderscore(document.GetCharAt(offset)))
                ++offset;

            return offset;
        }

        public static int FindWordStart(IDocument document, int offset)
        {
            var line = document.GetLineSegmentForOffset(offset);
            var lineOffset = line.Offset;
            while (offset > lineOffset && IsLetterDigitOrUnderscore(document.GetCharAt(offset - 1)))
                --offset;

            return offset;
        }

        // go forward to the start of the next word
        // if the cursor is at the start or in the middle of a word we move to the end of the word
        // and then past any whitespace that follows it
        // if the cursor is at the start or in the middle of some whitespace we move to the start of the
        // next word
        public static int FindNextWordStart(IDocument document, int offset)
        {
//            var originalOffset = offset;
            var line = document.GetLineSegmentForOffset(offset);
            var endPos = line.Offset + line.Length;
            // lets go to the end of the word, whitespace or operator
            var t = GetCharacterType(document.GetCharAt(offset));
            while (offset < endPos && GetCharacterType(document.GetCharAt(offset)) == t)
                ++offset;

            // now we're at the end of the word, lets find the start of the next one by skipping whitespace
            while (offset < endPos && GetCharacterType(document.GetCharAt(offset)) == CharacterType.WhiteSpace)
                ++offset;

            return offset;
        }

        // go back to the start of the word we are on
        // if we are already at the start of a word or if we are in whitespace, then go back
        // to the start of the previous word
        public static int FindPrevWordStart(IDocument document, int offset)
        {
//            var originalOffset = offset;
            if (offset > 0)
            {
                var line = document.GetLineSegmentForOffset(offset);
                var t = GetCharacterType(document.GetCharAt(offset - 1));
                while (offset > line.Offset && GetCharacterType(document.GetCharAt(offset - 1)) == t)
                    --offset;

                // if we were in whitespace, and now we're at the end of a word or operator, go back to the beginning of it
                if (t == CharacterType.WhiteSpace && offset > line.Offset)
                {
                    t = GetCharacterType(document.GetCharAt(offset - 1));
                    while (offset > line.Offset && GetCharacterType(document.GetCharAt(offset - 1)) == t)
                        --offset;
                }
            }

            return offset;
        }

        public static string GetLineAsString(IDocument document, int lineNumber)
        {
            var line = document.GetLineSegment(lineNumber);
            return document.GetText(line.Offset, line.Length);
        }

        public static int SearchBracketBackward(IDocument document, int offset, char openBracket, char closingBracket)
        {
            return document.FormattingStrategy.SearchBracketBackward(document, offset, openBracket, closingBracket);
        }

        public static int SearchBracketForward(IDocument document, int offset, char openBracket, char closingBracket)
        {
            return document.FormattingStrategy.SearchBracketForward(document, offset, openBracket, closingBracket);
        }

        /// <remarks>
        ///     Returns true, if the line lineNumber is empty or filled with whitespaces.
        /// </remarks>
        public static bool IsEmptyLine(IDocument document, int lineNumber)
        {
            return IsEmptyLine(document, document.GetLineSegment(lineNumber));
        }

        /// <remarks>
        ///     Returns true, if the line lineNumber is empty or filled with whitespaces.
        /// </remarks>
        public static bool IsEmptyLine(IDocument document, LineSegment line)
        {
            for (var i = line.Offset; i < line.Offset + line.Length; ++i)
            {
                var ch = document.GetCharAt(i);
                if (!char.IsWhiteSpace(ch))
                    return false;
            }

            return true;
        }

        private static bool IsWordPart(char ch)
        {
            return IsLetterDigitOrUnderscore(ch) || ch == '.';
        }

        public static string GetWordAt(IDocument document, int offset)
        {
            if (offset < 0 || offset >= document.TextLength - 1 || !IsWordPart(document.GetCharAt(offset)))
                return string.Empty;
            var startOffset = offset;
            var endOffset = offset;
            while (startOffset > 0 && IsWordPart(document.GetCharAt(startOffset - 1)))
                --startOffset;

            while (endOffset < document.TextLength - 1 && IsWordPart(document.GetCharAt(endOffset + 1)))
                ++endOffset;

            Debug.Assert(endOffset >= startOffset);
            return document.GetText(startOffset, endOffset - startOffset + 1);
        }
    }
}